Skip to content

Add pathtext recipe for text along a path#5596

Merged
jkrumbiegel merged 21 commits intomasterfrom
jk/pathtext
Apr 16, 2026
Merged

Add pathtext recipe for text along a path#5596
jkrumbiegel merged 21 commits intomasterfrom
jk/pathtext

Conversation

@jkrumbiegel
Copy link
Copy Markdown
Member

@jkrumbiegel jkrumbiegel commented Apr 15, 2026

Adds a pathtext recipe for placing String or RichText along a Vector{<:Point2} or BezierPath. For BezierPath inputs, glyph positions and tangents come from exact cubic-Bézier evaluation; arc-length quadrature is adapted from kurbo (MIT).

Also adds Ann.Styles.WithText, which wraps another annotation style and layers a pathtext label along the connection path.

Example

xs = range(0, 4π, length = 200)
path = Point2f.(xs, sin.(xs))

f = Figure(size = (900, 300))
ax = Axis(f[1, 1]; limits = ((0, 4π), (-2, 2)))
hidedecorations!(ax); hidespines!(ax)
lines!(ax, path; color = (:gray, 0.4), linewidth = 2)

pathtext!(ax, path; text = "text flowing along a sine wave",
    fontsize = 18, align = (:center, :bottom), offset = 6)

rt = rich("sin(x) = ", rich("Σ "; color = :crimson),
    "(−1)", superscript("n"), " x", superscript("2n+1"), " / (2n+1)!")
pathtext!(ax, path; text = rt, fontsize = 18,
    align = (:center, :top), offset = -6)

f
pathtext along a sine wave

Ann.Styles.WithText

annotation!(ax, [Point2f(1, 2)], [Point2f(5, 5)];
    text = [""],
    path = Ann.Paths.Arc(height = 0.4),
    style = Ann.Styles.WithText(Ann.Styles.LineArrow();
        text = rich("H", subscript("2"), "O → ",
            rich("products"; color = :crimson)),
        fontsize = 14),
    color = :steelblue, labelspace = :data, shrink = (5.0, 5.0))
annotation with WithText style

Checks

  • Reference images for align, RichText + offset, polyline vs BezierPath, :data vs :pixel space, and Ann.Styles.WithText
  • Docs page with attribute_examples

First working prototype. Supports:
- Vector{Point2} (with NaN separators) and BezierPath inputs
- :left/:center/:right or fractional alignment along path
- Per-character colors, strokecolor, strokewidth
- Perpendicular offset via polyline offsetting (miter-based)
- :data and :pixel path space (text always pixel-sized)
- Reactive re-layout on camera changes via register_projected_positions!
Replace linearized polyline sampling with direct cubic Bézier evaluation
when the input is a BezierPath. This gives smooth, continuous tangent
rotations instead of the piecewise-constant tangents from the 30-point
linearization.

Key additions:
- Cubic Bézier eval/deriv/second_deriv functions
- 8-point Gauss-Legendre arc-length quadrature
- Offset-curve arc length via curvature formula (speed·|1-d·κ|)
- Binary-search inverse arc-length for parameter recovery
- Control-point extraction/reassembly for projecting BezierPath to pixel
- Separate convert_arguments methods (no more PointBased trait)
Layout RichText using layout_text() to get per-glyph positions, colors,
fonts, and sizes. Each glyph is wrapped as a single-char RichText
carrying its own style, letting the child text! handle font/color/size
natively per block.

Example: pathtext!(ax, path; text = rich("A", rich("B"; color=:red, font=:bold)))
The glyph y-origins from layout_text (e.g. subscript below baseline,
superscript above) are now applied as perpendicular shifts from the
path at each glyph position.
layout_text returns absolute y-origins (baseline at ~ascender height,
not y=0). Subtract the baseline y so regular glyphs sit on the path
and sub/superscripts shift perpendicular relative to it.
…tion

align now accepts a (halign, valign) tuple like the text recipe:
- halign: :left, :center, :right, or Real fraction (along path)
- valign: :baseline, :bottom, :center, :top (perpendicular to path)

valign uses font ascender/descender metrics to shift the text
perpendicular to the path. offset remains as an additional manual
pixel shift on top of valign.
Each glyph's rotation is now computed from the chord between its start
and end positions on the path (spaced by the glyph's advance width),
rather than the instantaneous tangent at the origin. This gives wider
characters like "W" a rotation that better matches the arc they span.
- docs/src/reference/plots/pathtext.md with BezierPath, polyline,
  and RichText sub/superscript examples
- attribute_examples for text (String vs RichText), align (valign
  comparison), and offset
Five reference tests covering:
- align: halign (:left/:center/:right) and valign (:top/:center/:bottom)
- String with per-char color vector and RichText with sub/superscripts
- offset: perpendicular shift from path
- polyline with NaN sub-path gap, BezierPath with LineTo/CurveTo/EllipticalArc
- space = :data vs :pixel for the path coordinate system
When a glyph's start and end samples land on different sub-paths
(separated by a NaN in a polyline or a MoveTo in a BezierPath), use
the tangent at the start position instead of the chord, which would
otherwise connect unrelated points across the gap.
- Shrink all pathtext refimages; hide axis decorations for denser framing.
- Combine "string+richtext" and "offset" tests (both shown together on
  one curve with opposite offsets and per-char vs RichText styling).
- Shorten polyline and BezierPath demo text to fit their smaller axes.
@github-project-automation github-project-automation bot moved this to Work in progress in PR review Apr 15, 2026
@jkrumbiegel jkrumbiegel force-pushed the jk/pathtext branch 2 times, most recently from d7ad259 to 22b5767 Compare April 15, 2026 08:51
Wraps another annotation style (passed positionally) and layers
`pathtext` on top so a label follows the connection path. Adds a
reference test and an attribute_examples entry. Includes a
convert_attribute override for pathtext's `align` so PlotSpec doesn't
coerce the symbolic `(halign, valign)` tuple into the numeric
text-style align.
@MakieBot
Copy link
Copy Markdown
Collaborator

MakieBot commented Apr 15, 2026

Benchmark Results

SHA: 4351c550e58c02e88142bfdcdb5bd56ff8accff9

Warning

These results are subject to substantial noise because GitHub's CI runs on shared machines that are not ideally suited for benchmarking.

GLMakie
CairoMakie
WGLMakie

Split the text and path sides via inner-method dispatch:
  _layout_glyphs(text, ...) — AbstractString vs RichText
  _prepare_path_sampler(path, ...) — polyline vs BezierPath
The outer _pathtext_layout is now one method that orchestrates both.
…t-length

- _sample_bezierpath_at now binary-searches a cumulative arc-length
  table precomputed in _prepare_bezierpath, instead of scanning segments
  linearly per sample.
- _cubic_inv_arclen uses 20 bisection iterations instead of 30
  (≈1e-6 parameter precision, well below sub-pixel).
- _layout_glyphs computes x_positions with a single-pass accumulator
  instead of cumsum(advances) .- advances.
- total_text_len is now computed inside _place_glyphs_on_path instead
  of passed in (one fewer positional arg).
- Trimmed a few WHAT-comments; removed an unreachable return.
@jkrumbiegel jkrumbiegel marked this pull request as ready for review April 16, 2026 11:00
@jkrumbiegel jkrumbiegel merged commit d0f7cef into master Apr 16, 2026
26 of 32 checks passed
@jkrumbiegel jkrumbiegel deleted the jk/pathtext branch April 16, 2026 12:30
@github-project-automation github-project-automation bot moved this from Work in progress to Merged in PR review Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Merged

Development

Successfully merging this pull request may close these issues.

2 participants